<?php
/**
 * Here all framework core functions are defined.
 *
 * The most of the here defined functions are for better string handling.
 *
 * @author     Ulf Kadner <ulfikado@gmail.com>
 * @category   Beluga PHP5.3+ Framework
 * @copyright  (c) 2015, Ulfikado
 * @package    BF\Core
 * @since      2015-01-17 11:48
 * @version    0.1
 */

namespace BF
{

   /**
    * Returns the length of a string with a defined encoding.
    * Encapsulate PHPs {@link http://php.net/mb_strlen mb_strlen} function.
    *
    * @param      string $str The string
    * @param      string $charset Encoding of the string (defaults to 'UTF-8')
    * @return     int
    * @uses       Multibyte extension The function requires that PHP have the Multibyte extension mb_string enabled.
    * @since      v0.1
    */
   function strlen( $str, $charset = 'UTF-8' )
   {

      return \mb_strlen( $str, $charset );

   }

   /**
    * Extracts a sub string from $str with a defined encoding.
    *
    * @param      string $str The string to work with.
    * @param      int    $start Start index (0-n) where the extraction begins.
    * @param      int    $length Length og the substring to extract or all, if the default value NULL is used.
    *             It also can use a negative value.
    * @param      string $charset Encoding of the string (defaults to 'UTF-8')
    * @return     string
    * @uses       Multibyte extension The function requires that PHP have the Multibyte extension mb_string enabled.
    * @since      v0.1
    */
   function substr( $str, $start, $length = null, $charset = 'UTF-8' )
   {

      // Return a empty string, if $str is NULL
      if ( \is_null( $str ) )
      {
         return '';
      }

      // If no length of $str is defined get it
      if ( \is_null( $length ) )
      {
         $length = \BF\strlen ( $str, $charset ) - $start;
      }

      // return the substr result
      return \mb_substr( $str, $start, $length, $charset );

   }

   /**
    * Returns the first position (index 0-n) of $needle inside $str, or (bool)FALSE if $needle is not contained.
    *
    * @param  string $str      The string.
    * @param  string $needle   The sub string to locate in $str
    * @param  bool   $caseless Ignore the case? (defaults to FALSE)
    * @param  string $charset  Encoding of the string (defaults to 'UTF-8')
    * @param  int    $offset   Start position of search, or NULL
    * @return integer|FALSE        Returns the resulting index, or boolean FALSE if not found.
    * @uses   Multibyte extension The function requires that PHP have the Multibyte extension mb_string enabled.
    * @since  v0.1
    */
   function strpos( $str, $needle, $caseless = false, $charset = 'UTF-8', $offset = null )
   {

      // If a required parameter is wrong, return FALSE
      if ( \is_null( $str )
        || \is_null( $needle )
        || \strlen( $needle ) < 1
        || \strlen( $str ) < 1 )
      {
         return false;
      }

      if ( $caseless )
      {
         // getting the caseless position
         $result =  \mb_stripos( $str, $needle, $offset, $charset );
      }
      else
      {
         // Getting the position depending to the case
         $result = \mb_strpos( $str, $needle, $offset, $charset );
      }

      // if noting was found, return FALSE
      if ( $result < 0 )
      {
         return false;
      }

      return $result;

   }

   /**
    * Returns the last position (index 0-n) of $needle inside $str, or (bool)FALSE if $needle is not contained.
    *
    * @param  string $str      The string.
    * @param  string $needle   The sub string to locate in $str
    * @param  bool   $caseless Ignore the case? (defaults to FALSE)
    * @param  string $charset  Encoding of the string (defaults to 'UTF-8')
    * @return integer|FALSE    Returns the resulting index, or boolean FALSE if not found.
    * @uses   Multibyte extension The function requires that PHP have the Multibyte extension mb_string enabled.
    * @since  v0.1
    */
   function str_lastpos( $str, $needle, $caseless = false, $charset = 'UTF-8' )
   {

      // If a required parameter is wrong, return FALSE
      if ( \is_null( $str )
        || \is_null( $needle )
        || \strlen( $needle ) < 1
        || \strlen( $str ) < 1 )
      {
         return false;
      }

      if ( $caseless )
      {
         // getting the caseless position
         $idx =  \mb_strripos( $str, $needle, null, $charset );
      }
      else
      {
         // Getting the position depending to the case
         $idx = \mb_strrpos( $str, $needle, null, $charset );
      }

      // if noting was found, return FALSE
      if ( $idx < 0 )
      {
         return false;
      }

      return $idx;

   }

   /**
    * Returns all Positions of $needle in $str.
    *
    * @param  string $str      The string.
    * @param  string $needle   The sub string to locate in $str
    * @param  bool   $caseless Ignore the case? (defaults to FALSE)
    * @param  string $charset  Encoding of the string (defaults to 'UTF-8')
    * @return array|FALSE      Return a numeric indicated array with all positions of $needle in $str or (bool)FALSE if
    *                          nothing was found
    * @uses   Multibyte extension The function requires that PHP have the Multibyte extension mb_string enabled.
    * @uses   \BF\strpos
    * @uses   \BF\strlen
    * @since  v0.1
    */
   function str_positions( $str, $needle, $caseless = false, $charset = 'UTF-8' )
   {

      // Init the array of positions, to return
      $positions = array();

      // If nothing was found, return FALSE
      if ( false === ( $idx = \BF\strpos( $str, $needle, $caseless, $charset ) ) )
      {
         return false;
      }

      // Adding the first position to the postions result array
      $positions[] = $idx;

      // Getting the length of the needle string
      $needleLen = \BF\strlen( $needle, $charset );

      // Finding all next positions
      while ( false !== ( $next = \BF\strpos( $str, $needle, $caseless, $charset, $idx + $needleLen ) ) )
      {
         $idx = $next;
         $positions[] = $idx;
      }

      return $positions;

   }

   /**
    * Returns, if $str starts with $needle.
    *
    * @param  string  $str      The string to check.
    * @param  string  $needle   The string, where $str must starts with
    * @param  boolean $caseless Ignore the case? (defaults to FALSE)
    * @param  string  $charset Encoding of the string (defaults to 'UTF-8')
    * @return boolean Returns TRUE on success, FALSE otherwise.
    * @uses   Multibyte extension The function requires that PHP have the Multibyte extension mb_string enabled.
    * @uses   \BF\strpos
    * @since  v0.1
    */
   function str_startswith( $str, $needle, $caseless = false, $charset = 'UTF-8' )
   {

      return ( 0 === \BF\strpos( $str, $needle, $caseless, $charset ) );

   }

   /**
    * Returns, if $str ends with $needle.
    *
    * @param  string  $str      The string to check.
    * @param  string  $needle   The string, where $str must ends with
    * @param  boolean $caseless Ignore the case? (defaults to FALSE)
    * @param  string  $charset  Encoding of the string (defaults to 'UTF-8')
    * @return boolean Returns TRUE on success, FALSE otherwise.
    * @uses   Multibyte extension The function requires that PHP have the Multibyte extension mb_string enabled.
    * @uses   \BF\strlen
    * @uses   \BF\str_lastpos
    * @since  v0.1
    */
   function str_endswith( $str, $needle, $caseless = false, $charset = 'UTF-8' )
   {

      // If $str is not a string, or if $needle is no string, return false
      if ( !\is_string( $str ) || !\is_string( $needle ) )
      {
         return false;
      }

      // Getting the lengths of $needle and $str
      $nl = \BF\strlen( $needle, $charset );
      $sl = \BF\strlen( $str, $charset );

      // If $needle is empty, or if $needle is longer than $str, return FALSE
      if ( ( $nl < 1 ) || ( $sl < $nl ) )
      {
         return false;
      }

      return ( ($sl - $nl) == \BF\str_lastpos($str, $needle, $caseless, $charset) );

   }

   /**
    * Returns, if $str contains $needle.
    *
    * @param  string  $str      The string to check.
    * @param  string  $needle   he string, where $str must contains
    * @param  boolean $caseless Ignore the case? (defaults to FALSE)
    * @param  string  $charset  Encoding of the string (defaults to 'UTF-8')
    * @return boolean Returns TRUE on success, FALSE otherwise.
    * @uses   Multibyte extension The function requires that PHP have the Multibyte extension mb_string enabled.
    * @uses   \BF\strpos
    * @since  v0.1
    */
   function str_contains( $str, $needle, $caseless = false, $charset = 'UTF-8' )
   {

      return ( false !== \BF\strpos( $str, $needle, $caseless, $charset ) );

   }

   /**
    * Escapes a string for use as a XML element value. The characters &amp;&lt;&gt; will be converted to entities.
    *
    * @param  string $str The string
    * @return string
    * @since  v0.1
    */
   function mask_xml( $str )
   {

      return \str_replace(
         array( '&',     '<',    '>' ),
         array( '&amp;', '&lt;', '&gt;' ),
         $str
      );

    }

   /**
    * Escapes a string for use as XML element attribute value. The chars &amp;&lt;&gt;" will be converted to entities.
    *
    * @param  string $str The string
    * @return string
    * @since  v0.1
    */
   function mask_arg( $str )
   {

      return \str_replace(
         array( '&',     '<',    '>',    '"',      "'"  ),
         array( '&amp;', '&lt;', '&gt;', '&quot;', '&#39;' ),
         $str
      );

    }

   /**
    * Escapes the defined value, depending to the defined type.
    *
    * @param  mixed  $str  The value to escape
    * @param  string $type The escaping type (One of the ESCAPE_* constants.
    * @return string
    * @uses   \BF\mask_xml
    * @uses   \url_encode
    * @uses   \json_encode
    * @uses   \BF\mask_arg
    * @since  v0.1
    */
   function escape( $str, $type = \ESCAPE_HTML_ALL )
   {

      switch ( $type )
      {

         case \ESCAPE_HTML:
            return \BF\mask_xml( $str );

         case \ESCAPE_URL:
            return \urlencode( $str );

         case \ESCAPE_JSON:
            return \json_encode( $str );

         default:
            return \BF\mask_arg( $str );

      }

   }

   /**
    * Unescapes (HT|X)ML entities in a string.
    *
    * @param  string  $str  The string
    * @param  boolean $full Unescape all entities?
    * @return string
    * @since  v0.1
    */
   function unmask_xml( $str, $full = false )
   {

      // This strings will be replaced (if $full is TRUE it are regular expressions)
      $search = ! $full
         ? array(
            '&auml;', '&Auml;', '&ouml;', '&Ouml;', '&uuml;', '&Uuml;',
            '&lt;', '&gt;', '&quot;', '&#39;', '&amp;', '&deg;', '&szlig;' )
         : array(
            '~&(quot|#34|#034|#x22);~i',
            '~&(amp|#38|#038|#x26);~i',
            '~&(lt|#60|#060|#x3c);~i',
            '~&(gt|#62|#062|#x3e);~i',
            '~&(nbsp|#160|#xa0);~i',
            '~&(iexcl|#161);~i',
            '~&(cent|#162);~i',
            '~&(pound|#163);~i',
            '~&(copy|#169);~i',
            '~&(reg|#174);~i',
            '~&(deg|#176);~i',
            '~&(#39|#039|#x27);~',
            '~&(euro|#8364);~i',
            '~&a(uml|UML);~',
            '~&o(uml|UML);~',
            '~&u(uml|UML);~',
            '~&A(uml|UML);~',
            '~&O(uml|UML);~',
            '~&U(uml|UML);~',
            '~&szlig;~i',
            '~&#(\d{1,4});~e' );

      // The replacements depending to $full
      $replace = ! $full
         ? array(
            'ä', 'Ä', 'ö', 'Ö', 'ü', 'Ü', '<', '>', '"', "'", '&', '°', 'ß' )
         : array(
            '"', '&', '<', '>', ' ', \chr( 161 ), \chr( 162 ), \chr( 163 ), '©', '®', '°', \chr( 39 ), '€',
            'ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü', 'ß', 'chr($1)' );

      return ! $full
         ? \str_replace ( $search, $replace, $str )
         : \preg_replace( $search, $replace, $str );

   }

   /**
    * Trim the string $str to a max. length and appends, if trimming is required, a $appendix.
    *
    * <code>
    * // Example
    *
    * $str = 'My name is Max';
    * $newstr = \BF\str_max( $str, 11, 'Art' );
    * echo "'{$newstr}'";
    *
    * // Results in output: 'My name is Art'
    * </code>
    *
    * @param  string  $str       The string.
    * @param  integer $maxlength The resulting string max. length (incl. $appendix)
    * @param  string  $appendix  Optional appendix (Defaults to '…')
    * @param  string  $charset   Encoding of the string (defaults to 'UTF-8')
    * @return string
    * @uses   \BF\strlen
    * @uses   \BF\substr
    * @since  v0.1
    */
   function str_max( $str, $maxlength, $appendix = '…', $charset = 'UTF-8' )
   {

      // If no trimming is required return the origin string
      if ( \BF\strlen( $str, $charset ) <= $maxlength )
      {
         return $str;
      }

      return \BF\substr( $str, 0, $maxlength - \BF\strlen( $appendix, $charset ) ) . $appendix;

    }

   /**
    * str_replace with a caseless string handling.
    *
    * @param  string|array $search     What will be replaced?
    * @param  string       $replace    The Replacement
    * @param  string       $subject    The String to work with
    * @param  boolean      $useUnicode Use Unicode for internal regex? (defaults to TRUE)
    * @return string
    * @since  v0.1
    */
   function str_ireplace( $search, $replace, $subject, $useUnicode = true )
   {

      // Make sure $search is a array
      if ( ! \is_array( $search ) )
      {
         $search  = array( $search );
      }

      // Quote a search strings for use inside a regular expression
      for ( $i = 0, $j = \count( $search ); $i < $j; $i++ )
      {
         $search[$i] = \preg_quote( $search[ $i ] );
      }

      // Build the regular expression
      $search = '~(' . \join( '|', $search ) . ')~i' . ( $useUnicode ? 'u' : '' );

      return \preg_replace( $search, $replace, $subject );

   }

   /**
    * Removes all HTML/XML Markup from defined string.
    *
    * @param  string $str
    * @return string
    * @since  v0.1
    */
   function strip_tags( $str )
   {

      $search = array(
         '~<script[^>]*?>.*?</script>~siu',
         '~<style[^>]*?>.*?</style>~siu',
         '~<![\s\S]*?--[ \t\n\r]*>~u'
      );

      return \strip_tags( \preg_replace( $search, '', $str ) );

    }

   /**
    * Does a normal print_r but outputs inside &lt;pre&gt; Elements with a optional class-Attribute.
    *
    * @param  mixed  $value    The value to print out
    * @param  string $preClass Optional class attribute value of generated pre HTML element
    * @return string
    * @uses   \BF\mask_xml
    * @since  v0.1
    */
   function print_h( $value, $preClass = null )
   {

      echo '<pre',
         ( ! empty( $preClass ) ? " class=\"{$preClass}\">" : '>' ),
         \BF\mask_xml( \print_r( $value, true ) ),
         '</pre>';

   }

   /**
    * Explodes the defined string, at every line break (\r\n|\n|\r)
    *
    * @param  string $string
    * @return array
    * @since  v0.1
    */
   function splitLines( $string )
   {
      return \preg_split( "~(\r\n|\n|\r)~", $string );
   }

   /**
    * This is the framework autoloader function. Its always registered while the Framework is used.
    *
    * @param  string $classname
    * @return boolean
    */
   function autoloader( $classname )
   {

      // Remove leading Namespace separator
      $classname = \ltrim( $classname, '\\' );

      if ( ! \BF\str_startswith( $classname, 'BF\\' ) )
      {
         // If the classname does not start with BF\, this autoloader ignores it.
         return false;
      }

      // Split at namespace separator into elements
      $elements = explode( '\\', $classname );

      // Build the corresponding include class file path
      $file = \BF_FOLDER
            . \DIRECTORY_SEPARATOR
            . \join( \DIRECTORY_SEPARATOR, $elements )
            . '.class.php';

      if ( \file_exists( $file ) )
      {
         // Include the existing class file
         /** @noinspection PhpIncludeInspection */
         require $file;
         return true;
      }

      // Build the corresponding include interface file path
      $file = \BF_FOLDER
            . \DIRECTORY_SEPARATOR
            . \join( \DIRECTORY_SEPARATOR, $elements )
            . '.interface.php';

      if ( \file_exists( $file ) )
      {
         // Include the existing interface file
         /** @noinspection PhpIncludeInspection */
         require $file;
         return true;
      }

      return false;

   }

   \spl_autoload_register( '\BF\autoloader', true, true );

}

